Own 3D file format or Exporter plug-in for 3DS Max.
Using MaxSDK.
Note: for understating this material it is necessary to know foundations of OOP in C++, Windows programming of Dynamic Link Libraries (DLL), and foundations of work in 3DS max. Also you need to know foundations of 3D graphics and mathematics. For writing plug-ins for 3DS Max it is necessary to have: 3DS Max 3.x or higher with MaxSDK, Visual C++ 6.0. MaxSDK is supplied on the CD with 3DSMax.
Part I
Intro: How I decided to develop my own 3D file format
I had to make a program for a company, for demonstrating their project. It is a project for saving the Aral Sea, which is located in our country. So it was a 3D demo of their project about the Aral Sea.
I turned the music on loud and began.
I had a program (written by myself, following the documentation) that reads Discreet 3ds file format. But it wasn't suitable for me because of these reasons:
1. It doesn't have two or more texture cords.
2. It doesn't keep more than 64535 faces.
3. It's terrible and uncomfortable for reading.
I searched through the Internet for something suitable for my needs, but I didn't found anything. All formats have only one texture coordinate.
So I decided to develop my own file format. I named it VX and did it like this:
Listing 1: File format - VXLFormat.h (C++ header)
// VXLFormat.h
typedef unsigned char byte; //types definition
typedef unsigned short word;
typedef unsigned long dword;
// for type of texture
#define VXM_BUMP 1 //texture is a bumpmap
#define VXM_REFL 2 //texture is a reflectmap
typedef struct
{
word signature;
int numObjects;
int numFrames;
int numRecords;
} VXHeader; //header of file
typedef struct
{
byte Type; //Type of second texture
char Texture1[15]; //filename of first texture
char Texture2[15]; // filename of second texture
bool HaveTexture1; //material has first texture
bool HaveTexture2; //material has second texture
} VXMaterial; //record for material
typedef struct
{
float uv[2]; //first texture coordinates
float uv2[2]; //second texture coordinates
} VXTexCoord; //record for two texture coordinates
typedef struct
{
dword Offset; // offset in file for this object
dword numRecords; //number of vertexes
dword numFaces; //number of faces
char Name[15]; //name of object
VXTexCoord *TexCoords; //array of texture coordinates
} VXObject; //record for object
typedef struct
{
float n[3]; //normal
float v[3]; //vertex
} VXRecord; //record for vertex coordinates and normal
class VXFile //class for working with file
{
private:
//has no private :)
public:
VXFile::VXFile();
VXFile::~VXFile();
//some working stuff
bool circled;
unsigned int objNum, selected;
dword tempOffset;
VXHeader Head; //my header
VXMaterial *Materials; //array of materilas
VXObject *Objects; //array of objects
VXRecord *Records; //array of records
bool LoadVXFile(char *name); //procedure for loading file into structures
bool SaveVXFileFrom3DMax(ofstream * eFile); //void for exporter
};
You see? It's very easy, but it is suitable for using.
I think I must give some explanations for this.
First, why do I put the array of texture coordinates into the VXObject structure, and put vertex and normal data in a common array?
Because I wanted to add animation in future versions, so I decided to do so. You see in animation vertex and normal data are changing during the time line, and texture coordinates data don't change.
And here is the listing of procedures of this class:
Listing 2: Class subroutines - VXLFormat.cpp (C++ cpp)
#include "VXLFormat.h"
#include <fstream.h>
bool VXFile::LoadVXFile(char *name)
{
ifstream fFile;
fFile.open(name, ios::in | ios::binary);
fFile.read((char *)&Head, sizeof(Head));
Materials = new VXMaterial[Head.numObjects+1]; //creating materials array
Objects = new VXObject[Head.numObjects+1]; //creating objects array
//reading materials
fFile.read((char *)Materials, Head.numObjects*sizeof(VXMaterial));
//reading objects
for (int i=0; i<Head.numObjects; i++)
{
fFile.read((char *)&Objects[i], 21);
Objects[i].TexCoordinates = new VXTexCoord[Objects[i].numRecords];
fFile.read((char *)&Objects[i].TexCoordinates[0], sizeof(VXTexCoord)*Objects[i].numRecords);
}
Records = new VXRecord[Head.numRecords+1]; //creating vertexes and normals array
// reading array
fFile.read((char *)Records, Head.numRecords*sizeof(VXRecord));
fFile.close();
return true;
}
VXFile::VXFile() //constructor
{
Head.numFrames = 0;
Head.numObjects = 0;
Head.numRecords = 0;
tempOffset = 0;
circled = false;
objNum = 0;
selected =0;
}
VXFile::~VXFile() //destructor
{
Head.numFrames = 0;
Head.numObjects = 0;
Head.numRecords = 0;
tempOffset = 0;
circled = false;
objNum = 0;
selected = 0;
delete Materials;
delete Objects;
delete Records;
}
And now you must see why it is suitable for using: all the data is located in order, and reading it is very easy. After you loaded the file, you may use it fast and easily both in DirectX and in OpenGL. It is very comfortable for buffer working, like in DirectX (in OpenGL it is possible only if the GL_EXT_vertex_array extension is supported by your video card).
Part II
Exporter: How can I write 3D models in a file of my own format?
Yeah, I thought about it too. I had three ways: to write my own 3D modeling program, to write a converter of another format and use one of the existing 3D programs. I chose the second way. I decided to write a plug-in for 3DS max 5.0 to which I had access, and began to study the construction of 3DS Max.
So I found:
1. 3DS Max has MaxSDK.
2. All plug-ins of 3DS Max are located in the \plug-ins\ directory.
3. MaxSDK has help and something named "Sparks Archive"
So, what are those things? - you'll ask me. MaxSDK is 3DS Max Software Development Kit - set of libraries and C++ header files for compiling plug-ins for 3DS max. As written in the SDK's help: "The 3ds max Software Development Kit (SDK) is an object-oriented programming library for creating plug-in applications for 3ds max. The SDK provides a comprehensive set of classes that developers can combine and extend to create seamlessly integrated plug-in applications. Using the SDK one can create a great variety of plug-ins. In fact, much of 3ds max itself is written as plug-in applications."
Sparks Archive is something like FAQ on MaxSDK, there is a lot of useful information.
It has a set of examples for help in developing too. All plug-ins for 3DS Max are DLL (Windows Dynamic Link Library), but extensions of their filenames are different from DLL. All plug-ins are determined by extensions, so:
*.dli - plug-ins for import
*.dle - plug-ins for export
*.dlc - controller plug-ins
*.dlu - utility plug-ins
*.dlm - modifier plug-ins
and so on.
So plug-in for 3DS Max is DLL with corresponding extension, but then I know that the file extension isn't very important for 3DS Max to determine the type of plug-in.
Exporter: Internal structure of Max's plug-in
Let's go into the \maxsdk\samples\impexp\ directory and look at it. It contains sources of plug-ins for import/export for Max. It contains the following sources:
objimp.dsw - MSVC project for importing *.obj file
dxfimp.dsw - MSVC project for importing AutoCAD *.dfx file
dxfexp.dsw - MSVC project for exporting AutoCAD *.dfx file
aiimp.dsw - MSVC project for importing *. ai file
aiexp.dsw - MSVC project for exporting *. ai file
3dsimp.dsw - MSVC project for importing Discreet *.3ds file
3dsexp.dsw - MSVC project for exporting Discreet *.3ds file
We need a plug-in for export, so let's open 3dsexp.dsw and look at it attentively. I know you will say: "What the #@$% is this?!?!?! I don't understand anything." But there is nothing complicated.
Let's examine the structure of a plug-in. From C++ point of view, a plug-in for Max is an object of a special class that is stored in a DLL. Any DLL may store more that one class. Let's have a good look at the subroutines:
- DllMain() - standard windows function for DLL initializing.
- LibNumberClasses() - than function returns the number of classes stored in this DLL.
- LibVersion() - returns the version of Max for which the plug-in is.
- LibDescription() - returns plug-in description string
- LibClassDesc() - that's the most important function. It returns a pointer to the object, which is named descriptor of class for every plug-in in that DLL. That object describes the properties of every plug-in class and the way of its creating.
So let's open Microsoft Visual C++ 6.0 and create a new C++ Win32 DLL project. Name it MyPlugin.
Listing 3: 3DS Max plug-in - MyPlugin.cpp
#include "MyPlugin.h"
HINSTANCE hInstance;
bool ControlsInit = false;
//Standart DLL entry point
BOOL APIENTRY DllMain(HINSTANCE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
hInstance = hModule;
if (!ControlsInit)
{
ControlsInit = true;
InitCustomControls(hInstance);
InitCommonControls();
}
return TRUE;
}
// than function returns number of classes stored in this DLL
__declspec(dllexport) int LibNumberClasses()
{
return 1;
}
// returns pointer to the description class number i
__declspec(dllexport) ClassDesc *LibClassDesc(int i)
{
switch (i)
{
case 0: return &PluginDesc;
default: return 0;
}
}
//returns description
__declspec(dllexport) const TCHAR *LibDescription()
{
return _T(GetString(IDS_PLUG-IN_NAME)); //gets it from string list resource
}
//returns version
__declspec(dllexport) ULONG LibVersion()
{
return VERSION_3DSMAX;
}
The next listing will be a little complicated. It is the file MyPlugin.h. There we have a description of DescripionClass, exporter class and special for exporters - SceneSaver. There it is:
Listing 4: 3DS Max plug-in - MyPlugin.h
#ifndef _MY_MAX_PLUG_
#define _MY_MAX_PLUG_
#include <windows.h>
#include <Max.h> //don't forget to include Max.h
#include <fstream.h>
#include <stdmat.h>
#include <string>
#include "resources.h"
#define MYEXP_CLASS_ID Class_ID(0x70af5fa1, 0x49f75422)
//unique identificator for every plug-in,
//it can be generated by gencid.exe utility in the \maxsdk\help\ directory.
static TCHAR* GetString(int id)
{
static TCHAR stBuf[255];
if (hInstance)
return LoadString(hInstance, id, stBuf, 255) ? stBuf : NULL;
return NULL;
}
class MyExp : public SceneExport
//exporter class, inherited from SceneExport from MaxSDK library
{
friend INT_PTR CALLBACK ExportOptionsDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
public:
int ExtCount() { return 1; }
const TCHAR* Ext(int i)
{ if (i == 0) return _T(GetString(IDS_PLUG-IN_EXT1)); else return _T(""); }
const TCHAR* LongDesc() { return _T(GetString(IDS_LONG_DESC)); }
//returns long description of plugin
const TCHAR* ShortDesc() { return _T(GetString(IDS_SHORT_DESC)); }
//returns short description of plugin
const TCHAR* AuthorName() { return _T(GetString(IDS_AUTHOR_NAME)); }
//returns author name
const TCHAR* CopyrightMessage() { return _T(GetString(IDS_COPYRIGHT)); }
//returns copyright message
const TCHAR* OtherMessage1() { return _T(""); }
const TCHAR* OtherMessage2() { return _T(""); }
unsigned int Version() { return 100; }
// returns version in xx.xx * 100 format, if version is 1.00 it must return 100
void ShowAbout(HWND hWnd){ MessageBox(hWnd, GetString(IDS_ABOUT), "About", MB_OK); }
//shows about box
BOOL SupportsOptions(int ext, DWORD options) {return 1;}
//supported options
//============= that function does export===========================
int DoExport(const TCHAR *name, ExpInterface *ei,
Interface *i, BOOL suppressPromts = FALSE, DWORD options = 0);
MyExp() {;};
virtual ~MyExp() {;}
};
class DescripionClass : public ClassDesc //description class
{
public:
int IsPublic() { return 1; }//is it for user? :)
void* Create(BOOL Loading = FALSE) { return new MyExp; }
//returns pointer to the new class object
const TCHAR * ClassName() { return _T(GetString(IDS_CLASS_NAME)); }
//reurns class name
SClass_ID SuperClassID() { return SCENE_EXPORT_CLASS_ID; }
//returns SCENE_EXPORT_CLASS_ID - means that this is exporter
Class_ID ClassID() { return MYEXP_CLASS_ID; }
//returns unique class id
const TCHAR * Category() { return _T(""); }
//in what toolbar it has it's button (it hasn't)
};
static DescripionClass PluginDesc; //object which returns LibClassDesc() function
typedef std::string remString; //temporary-work string
class SceneSaver: public ItreeEnumProc //that class is for saving Max's scene to file
{
public:
// Main functions
int callback(INode *node); //that function is called for export
void ProcNode(INode *node); //that function is called for process
//one node from scene
};
// Standalone functions
TriObject *GetTriObjFromNode(INode *node, int &deleteIt);
//gets triangle mesh from Max's node
remString ExtractFileName(remString filename);
#endif
I think there can't be any questions except "What's a class SceneSaver, a class ItreeEnumProc and their functions?" All these things I will tell you in the next listing.
So how does the exporter work? First of this I want to tell you some theory about the architecture of Max scene. Every object on the Max scene, in code, presents itself as a geometric conveyor that has a base geometric object in its foundation and then turns to the modified object in the output. For every object there is a link to this conveyor, which called node. With help of this node we can get all information about this object: material, result mesh (called TriObject), etc. From mesh we can receive vertex coordinates, normals and so on. If you want to learn more about it you can read about geometry pipeline system of Max in MaxSDK help. So for exporting scene we must parse all of scene nodes and take only suitable information.
We also will need information about how Max stores geometry and texture coordinates.
First we will take TriObject from node, then from TriObject we can get all geometric data.
So - the last listing:
Listing 5: 3DS Max plug-in - Exporter.cpp
#include "MyPlugin.h" //using our plug-in header
#include "VXLFormat.h" //using our 3D file-format class
Interface *interf; //this is interface from what we're gonna get nodes
static SceneSaver TreeEnum; //enumerator of scene from which called callback function
static ofstream fFile; //our file for writing
static VXFile expFile; // our format file
bool VXFile::SaveVXFileFrom3DMax(ofstream * eFile) //our function for writing file
{
TCHAR buf[255];
Head.signature = 1;
Head.numFrames = 0;
fFile.write((char *)&Head, sizeof(Head));
fFile.write((char *)Materials, Head.numObjects*sizeof(VXMaterial));
for (int i=0; i<Head.numObjects; i++)
{
fFile.write((char *)&Objects[i], 21);
fFile.write((char *)Objects[i].TexCoords, Objects[i].numRecords*sizeof(VXTexCoord));
for (int j=0; j<Objects[i].numRecords; j++)
{
sprintf(buf, "\n\nN: %d U: %f; V: %f;", j, Objects[i].TexCoords[j].uv[0],
Objects[i].TexCoords[j].uv[1]);
AddToMsgList(msgList, buf);
}
}
fFile.write((char *)Records, Head.numRecords*sizeof(VXRecord));
Head.numFrames = 0;
Head.numObjects = 0;
Head.numRecords = 0;
tempOffset = 0;
circled = false;
objNum = 0;
selected =0;
return true;
}
//that function does parsing the interface for scene
// I used two times parsing because first I want to know number of objects
// and then I'll get all data
int MyExp::DoExport(const TCHAR *name, ExpInterface *ei, Interface *i,
BOOL suppressPromts , DWORD options )
{
fFile.open(name, ios::out | ios::binary);
interf = i;
expFile.circled = false; //it is first parse or second?
ei->theScene->EnumTree(&TreeEnum);
//at this time called function callback, which is call ProcNode
// and we count number ofobjects in scene
expFile.circled = true;
//recording all data in our class strucure
expFile.Head.numObjects = expFile.objNum;
expFile.Objects = new VXObject[expFile.objNum+1];
expFile.Materials = new VXMaterial[expFile.objNum+1];
expFile.Records = new VXRecord[expFile.tempOffset+1];
expFile.Head.numRecords = expFile.tempOffset;
expFile.tempOffset = 0;
//do parsing second time
ei->theScene->EnumTree(&TreeEnum);
//save file
expFile.SaveVXFileFrom3DMax(&fFile);
//close file
fFile.close();
return 1;
}
//this procedure called when scene interface calls EnumTree
int SceneSaver::callback(INode *node) {
ProcNode(node); //call ProcNode
return TREE_CONTINUE;
}
void SceneSaver::ProcNode(INode *node) //parse all data
{
int numF, numV, numTV, CurrHeader, zero = 0, debug = 0xAA, Del;
streampos NextNode, VertexPointer, FacePointer, TmpPos;
TCHAR buf[255];
Matrix3 tm;
Point3 v;
// Get TriObject from node
TriObject *TObj;
TObj = GetTriObjFromNode(node, Del);
if (!expFile.circled) //if it is first parse
{
expFile.objNum++;
expFile.tempOffset+=(3*TObj->mesh.numFaces);
TObj->mesh.buildNormals(); //to build normals
}
else //if it is second parse
{
if (!TObj) return;
numF = TObj->mesh.numFaces; //got number of faces
numV = TObj->mesh.numVerts; //got number of faces
numTV = TObj->mesh.numTVerts; //got number of texture coordinates
expFile.Objects[expFile.selected].numRecords = numF*3;
expFile.Objects[expFile.selected].numFaces = numF;
sprintf(expFile.Objects[expFile.selected].Name, node->GetName());
//fot object name
expFile.Objects[expFile.selected].TexCoords =
new VXTexCoord[expFile.Objects[expFile.selected].numRecords];
tm = node->GetObjTMAfterWSM(interf->GetTime());
// this line does very important thing -
// it took object transformation matrix after all modifiers applied
// in current frame$
// besides to export animated object you may do many parses
// and change time every time
Point3 nCoord;
expFile.Objects[expFile.selected].Offset = expFile.tempOffset;
//calculate offset in file for vertices and normals data
for (int i = 0; i < TObj->mesh.numFaces; i++)
{
for (int j=0; j<3; j++)
{
Face face = TObj->mesh.faces[i];
TVFace tvFace = TObj->mesh.tvFace[i];
v = tm * TObj->mesh.verts[TObj->mesh.faces[i].v[j]];
v = v/100;
expFile.Records[expFile.tempOffset].v[0] = v.x;
expFile.Records[expFile.tempOffset].v[1] = v.z;
expFile.Records[expFile.tempOffset].v[2] = v.y;
nCoord = TObj->mesh.getNormal(TObj->mesh.faces[i].v[j])/100;
expFile.Records[expFile.tempOffset].n[0] = nCoord.x;
expFile.Records[expFile.tempOffset].n[1] = nCoord.z;
expFile.Records[expFile.tempOffset].n[2] = nCoord.y;
UVVert tvert = TObj->mesh.getTVert(tvFace.t[j]);
if (TObj->mesh.numTVerts != 0)
{
expFile.Objects[expFile.selected].TexCoords[expFile.tempOffset].uv[0]
= tvert.x;
expFile.Objects[expFile.selected].TexCoords[expFile.tempOffset].uv[1]
= tvert.y;
}
expFile.tempOffset ++;
}
}
//Got all triangle object data
// Get diffuse material texture name
Mtl *m = node->GetMtl();
if (!m) return;
// See if it's a standart material
if (m->ClassID() != Class_ID(DMTL_CLASS_ID, 0))
{
expFile.Materials[expFile.selected].HaveTexture1 = false;
expFile.Materials[expFile.selected].HaveTexture2 = false;
}
Texmap *tmap = m->GetSubTexmap(ID_DI);
if ((!tmap)||(tmap->ClassID() != Class_ID(BMTEX_CLASS_ID, 0)))
{
expFile.Materials[expFile.selected].HaveTexture1 = false;
}
else
{
expFile.Materials[expFile.selected].HaveTexture1 = true;
// If bitmap exists
BitmapTex *bmt = (BitmapTex *)tmap;
// Write name of bitmap to file;
strcpy((char *)expFile.Materials[expFile.selected].Texture1,
ExtractFileName(bmt->GetMapName()).data());
}
if (Texmap *tmap = m->GetSubTexmap(ID_BU)) //is it bumpmap?
{
expFile.Materials[expFile.selected].HaveTexture2 = true;
// If bitmap exists
BitmapTex *bmt = (BitmapTex *)tmap;
// Write name of bitmap to file;
strcpy((char *)expFile.Materials[expFile.selected].Texture2,
ExtractFileName(bmt->GetMapName()).data());
expFile.Materials[expFile.selected].Type = VXM_BUMP;
} else if (Texmap *tmap = m->GetSubTexmap(ID_RL)) //is it reflect map?
{
expFile.Materials[expFile.selected].HaveTexture2 = true;
// If bitmap exists
BitmapTex *bmt = (BitmapTex *)tmap;
// Write name of bitmap to file;
strcpy((char *)expFile.Materials[expFile.selected].Texture2,
ExtractFileName(bmt->GetMapName()).data());
expFile.Materials[expFile.selected].Type = VXM_REFL;
}
expFile.selected++;
}
}
//function for taking triangle object from scene node
TriObject *GetTriObjFromNode(INode *node, int &deleteIt)
{
deleteIt = FALSE;
Object *obj = node->EvalWorldState(interf->GetTime()).obj;
if (obj->CanConvertToType(Class_ID(TRIOBJ_CLASS_ID,0)))
{
TriObject *tri = (TriObject *) obj->ConvertToType(interf->GetTime(),
Class_ID(TRIOBJ_CLASS_ID, 0));
if (obj != tri) deleteIt = TRUE;
return tri;
}
else return NULL;
}
//function for extracting filename from full path
remString ExtractFileName(remString filename)
{
if (filename.size() == 0) return "";
int i = filename.size();
remString buf;
while((filename[i] != '\\') && (i > 0))
{
buf = filename[i--] + buf;
}
return buf;
}
I think that's all for this time. I tried to explain this topic very simplified. For any questions, comments, bug reports, opinions,... contact me.